﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
using Devtm.Generator.ModelStacks;
using Devtm.Common;

namespace Devtm.Generator.Writers
{

    [Flags]
    public enum propertyNum
    {
        All = 3,
        Get = 1,
        Set 
    }

    public class CodeWriter 
        : IWriter, IStackItem
    {


        public CodeWriter()
        {

        }

        public void Initialize(Project project, string extension = "")
        {

            this.project = project;

            if (string.IsNullOrEmpty(extension))
                this.Extension = project.Extension;
            else
                this.Extension = extension;

            if (!this.Extension.StartsWith("."))
                this.Extension = "." + this.Extension;


            CompileUnit = new CodeCompileUnit();
            CompileUnit.ReferencedAssemblies.Add(typeof(string).AssemblyQualifiedName);

            Namespaces = new CustomStack<CodeNamespace>(this);
            Types = new CustomStack<CodeTypeDeclaration>(this);
            Members = new CustomStack<CodeTypeMember>(this);
            Codes = new CustomStack<CodeStatementCollection>(this);

        }



        public string Extension { get; set; }


        public System.IO.FileInfo File { get; set; }

        private CodeCompileUnit CompileUnit;
        internal CustomStack<CodeTypeDeclaration> Types;
        internal CustomStack<CodeTypeMember> Members;
        internal CustomStack<CodeStatementCollection> Codes;
        internal CustomStack<CodeNamespace> Namespaces;
        private Project project;


        
        public bool Save(FileInfo file)
        {
            File = file;
            return Save();
        }

        public bool Save()
        {

            if (File.IsNull())
                throw this.NullReferenceException("Writer.File");

            File = File.Extension == Extension
               ? File
               : new FileInfo(File.FullName + Extension);


            if (CompileUnit.Namespaces.Count > 0)
            {

                if (!File.Directory.Exists)
                    File.Directory.Create();

                var language = CodeDomProvider.GetLanguageFromExtension(this.File.Extension);

                var provider = CodeDomProvider.CreateProvider(language);
                var infos = CodeDomProvider.GetCompilerInfo(language);
                
                var stream = new StreamWriter(File.FullName, false);
                var options = new CodeGeneratorOptions() { BlankLinesBetweenMembers = true, BracingStyle = "C" };

                CompileUnit.Namespaces[0].Import("System");

                using (IndentedTextWriter tw = new IndentedTextWriter(stream, "    "))
                {
                    provider.GenerateCodeFromCompileUnit(CompileUnit, tw, options);
                    tw.Flush();
                    tw.Close();
                    return true;
                }

            }
            else
            {
                if (File.Exists)
                    File.Delete();
            }

            return false;

        }


        public StackContainer<System.CodeDom.CodeNamespace> AddNamespace(string @namespace)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1}", @namespace, "Namespace", Tags.CreationNamespace, Tags.CodeDom);

            if (@namespace.IsNullOrEmpty())
                throw this.NullReferenceException("namespace");


            CodeNamespace n = CurrentNamespace = new CodeNamespace(@namespace);
            var result = Namespaces.AddBlock(n);
            
            CompileUnit.Namespaces.Add(n);            
            //Namespaces.Push(n);


            return result;
        }

        public CodeNamespace LastNamespace { get; set; }
        public CodeNamespace CurrentNamespace { get; private set; }

        public CodeTypeDeclaration LastType { get; private set; }
        public CodeTypeDeclaration CurrentType { get; private set; }

        public CodeTypeMember LastMember { get; private set; }
        public CodeTypeMember CurrentMember { get; private set; }

        public CodeStatementCollection CurrentStatement { get; private set; }




        public void ImplementINotifyPropertyChanging()
        {

            CurrentType.BaseTypes.Add(typeof(System.ComponentModel.INotifyPropertyChanging).TypeReference());

            CurrentType.Members.Add(new CodeMemberEvent() { Attributes = MemberAttributes.Public, Name = "PropertyChanging", Type = typeof(System.ComponentModel.PropertyChangingEventHandler).TypeReference() });


            using (var met = AddMethod(typeof(void).TypeReference(), "Changing", MemberAttributes.Family))
            {
                met.Item.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string).TypeReference(), "propertyName"));
                var If = new CodeConditionStatement(new CodeBinaryOperatorExpression(new CodeVariableReferenceExpression("PropertyChanging"), CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(null)));
                met.Item.Statements.Add(If);
                If.TrueStatements.Add(new CodeMethodInvokeExpression(null, "PropertyChanging", new CodeThisReferenceExpression(), new CodeObjectCreateExpression(typeof(System.ComponentModel.PropertyChangingEventArgs).TypeReference(), new CodeArgumentReferenceExpression("propertyName"))));
            }

        }


        public void ImplementINotifyPropertyChanged()
        {

            CurrentType.BaseTypes.Add(typeof(System.ComponentModel.INotifyPropertyChanged).TypeReference());
            CurrentType.Members.Add(new CodeMemberEvent() { Attributes = MemberAttributes.Public, Name = "PropertyChanged", Type = typeof(System.ComponentModel.PropertyChangedEventHandler).TypeReference() });

            using (var met = AddMethod(typeof(void).TypeReference(), "Changed", MemberAttributes.Family))
            {
                met.Item.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string).TypeReference(), "propertyName"));
                var If = new CodeConditionStatement(new CodeBinaryOperatorExpression(new CodeVariableReferenceExpression("PropertyChanged"), CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(null)));
                met.Item.Statements.Add(If);
                If.TrueStatements.Add(new CodeMethodInvokeExpression(null, "PropertyChanged", new CodeThisReferenceExpression(), new CodeObjectCreateExpression(typeof(System.ComponentModel.PropertyChangedEventArgs).TypeReference(), new CodeArgumentReferenceExpression("propertyName"))));
            }

        }


        public IBoxStack<CodeTypeDeclaration> AddStruct(string className, MemberAttributes memberAttribute = MemberAttributes.Public, string inheritFrom = "")
        {
            this.Log(LogLevelEnm.Trace, "Create {0} {1}", className, "Struct", Tags.CreationStruct, Tags.CodeDom);

            if (className.IsNullOrEmpty())
                throw this.ArgumentNull("className");

            var t = CurrentType = CurrentNamespace.AddStruct(memberAttribute, className, inheritFrom);
            var result = Types.AddBlock(t);
            return result;
        }
        

        public IBoxStack<CodeTypeDeclaration> AddEnum(string className, MemberAttributes memberAttribute = MemberAttributes.Public, Type inheritFrom = null)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1}", className, "Enum", Tags.CreationEnum, Tags.CodeDom);

            if (className.IsNullOrEmpty())
                throw this.ArgumentNull("className");

            var t = CurrentType = CurrentNamespace.AddEnum(memberAttribute, className, inheritFrom);
            var result = Types.AddBlock(t);
            return result;
        }


        public IBoxStack<CodeTypeDeclaration> AddInterface(string className, MemberAttributes memberAttribute = MemberAttributes.Public, string inheritFrom = "")
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1}", className, "Interface", Tags.CreationInterface, Tags.CodeDom);

            if (className.IsNullOrEmpty())
                throw this.ArgumentNull("className");

            var t = CurrentType = CurrentNamespace.AddInterface(memberAttribute, className, inheritFrom);
            var result = Types.AddBlock(t);
            return result;
        }


        public IBoxStack<CodeTypeDeclaration> AddClass(string className, MemberAttributes memberAttribute = MemberAttributes.Public)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1}", className, "Class", Tags.CreationClass, Tags.CodeDom);

            if (className.IsNullOrEmpty())
                throw this.ArgumentNull("className");

            var t = CurrentType = CurrentNamespace.AddClass(memberAttribute, className, null, true);
            t.Attributes = memberAttribute;
            var result = Types.AddBlock(t);
            return result;
        }


        public IBoxStack<CodeTypeDeclaration> AddClass(string className, MemberAttributes memberAttribute = MemberAttributes.Public, CodeTypeReference inheritFrom = null)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1}", className, "Class", Tags.CreationClass, Tags.CodeDom);

            if (className.IsNullOrEmpty())
                throw this.ArgumentNull("className");

            var t = CurrentType = CurrentNamespace.AddClass(memberAttribute, className, inheritFrom);
            t.Attributes = memberAttribute;
            var result = Types.AddBlock(t);
            return result;
        }


        public IBoxStack<CodeMemberProperty> AddProperty(string typeName, string propertyName, MemberAttributes attributes = MemberAttributes.Public)
        {

            if (typeName.IsNullOrEmpty())
                throw this.ArgumentNull("typeName");

            if (propertyName.IsNullOrEmpty())
                throw this.ArgumentNull("propertyName");

            var type = Devtm.Writers.TypeReference.Get(typeName);
            return AddProperty(type, propertyName, attributes);
        }


        public CodeMemberProperty AddPropertyGetSet(string typeName, string propertyName, propertyNum member = propertyNum.All, MemberAttributes attributes = MemberAttributes.Public)
        {

            CodeMemberProperty p;

            if (typeName.IsNullOrEmpty())
                throw this.ArgumentNull("typeName");

            if (propertyName.IsNullOrEmpty())
                throw this.ArgumentNull("propertyName");

            CodeMemberField field = null;

            if (CurrentType.IsClass)
            {
                var m = MemberAttributes.Private;
                
                if ((attributes & MemberAttributes.Static) == MemberAttributes.Static)
                    m = m | MemberAttributes.Static;

                field = AddField(typeName, "f_" + propertyName, m);

            }

            using (var prop = AddProperty(typeName, propertyName, attributes))
            {
                
                p = prop.Item;

                if (CurrentType.IsInterface)
                {
                    if ((member & propertyNum.Get) == propertyNum.Get)
                        prop.Item.GetStatements.Add(new CodeSnippetExpression("get;"));

                    if ((member & propertyNum.Set) == propertyNum.Set)
                        prop.Item.SetStatements.Add(new CodeSnippetExpression("set;"));
                }
                else if (CurrentType.IsClass)
                {

                    var f = field.Reference();

                    if ((member & propertyNum.Get) == propertyNum.Get)
                        prop.Item.GetStatements.Return(f);

                    if ((member & propertyNum.Set) == propertyNum.Set)
                        prop.Item.SetStatements.Set(f, "value".Var());

                }

            }

            return p;

        }


        public CodeMemberProperty AddPropertyGetSet(CodeTypeReference type, string propertyName, propertyNum member = propertyNum.All, MemberAttributes attributes = MemberAttributes.Public)
        {

            CodeMemberProperty p;

            if (propertyName.IsNullOrEmpty())
                throw this.ArgumentNull("propertyName");

            using (var prop = AddProperty(type, propertyName, attributes))
            {

                p = prop.Item;

                if ((member & propertyNum.Get) == propertyNum.Get)
                    prop.Item.GetStatements.Add(new CodeSnippetExpression("get;"));

                if ((member & propertyNum.Set) == propertyNum.Set)
                    prop.Item.SetStatements.Add(new CodeSnippetExpression("set;"));

            }

            return p;

        }


        public IBoxStack<CodeMemberProperty> AddPropertyWithGetSet(string typeName, string propertyName, MemberAttributes attributes = MemberAttributes.Public)
        {
            return AddPropertyWithGetSet(typeName.TypeReference(), propertyName, attributes);
        }


        public IBoxStack<CodeMemberProperty> AddPropertyWithGetSet(CodeTypeReference type, string propertyName, MemberAttributes attributes = MemberAttributes.Public)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1} {2}", type.BaseType, propertyName, "property", Tags.CreationProperty, Tags.CodeDom);

            if (type.IsNull())
                throw this.ArgumentNull("typeName");

            if (propertyName.IsNullOrEmpty())
                throw this.ArgumentNull("propertyName");


            
            var prop = AddProperty(type, propertyName, attributes);

            var field = AddField(type, "m_" + propertyName, MemberAttributes.Private);
            var _f = new CodeFieldReferenceExpression(This(), field.Name);

            prop.Item.GetStatements.Add(new CodeMethodReturnStatement(_f)); ;

            bool changed = false;
            bool changing = false;
            foreach (CodeObject item in CurrentType.BaseTypes)
            {
                System.CodeDom.CodeTypeReference t = item as System.CodeDom.CodeTypeReference;
                if (t != null)
                {
                    if (t.BaseType == typeof(System.ComponentModel.INotifyPropertyChanged).FullName)
                        changed = true;
                    else if (t.BaseType == typeof(System.ComponentModel.INotifyPropertyChanging).FullName)
                        changing = true;
                }
            }

            if (changed || changing)
            {
                var If = new CodeConditionStatement(new CodeBinaryOperatorExpression(_f, CodeBinaryOperatorType.IdentityInequality, new CodeVariableReferenceExpression("value")));
                prop.Item.SetStatements.Add(If);
                if (changing)
                    If.TrueStatements.Add(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(null, "Changing"), new CodePrimitiveExpression(propertyName)));
                If.TrueStatements.Set(_f, CodeHelper.Value());
                if (changed)
                    If.TrueStatements.Add(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(null, "Changed"), new CodePrimitiveExpression(propertyName)));
            }
            else
            {
                prop.Item.SetStatements.Set(_f, CodeHelper.Value());
            }
            return prop;
        }


        public IBoxStack<CodeMemberProperty> AddProperty(CodeTypeReference type, string propertyName, MemberAttributes attributes = MemberAttributes.Public)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1} {2}", type.BaseType, propertyName, "property", Tags.CreationProperty, Tags.CodeDom);


            if (type.IsNull())
                throw this.ArgumentNull("type");

            if (propertyName.IsNullOrEmpty())
                throw this.ArgumentNull("propertyName");

            CodeMemberProperty member = new CodeMemberProperty();
            CurrentMember = member;
            CurrentType.Members.Add(member);
            IBoxStack<CodeTypeMember> result = Members.AddBlock(member);
            member.Name = propertyName;
            member.Type = type;
            member.Attributes = attributes;
            return result.ConvertIn<CodeMemberProperty>();
        }


        public CodeMemberField AddField(string typeName, string fieldName, MemberAttributes attributes = MemberAttributes.Public)
        {

            if (typeName.IsNullOrEmpty())
                throw this.ArgumentNull("typeName");

            if (fieldName.IsNullOrEmpty())
                throw this.ArgumentNull("fieldName");

            var type = Devtm.Writers.TypeReference.Get(typeName);
            return AddField(type, fieldName, attributes);
        }


        public CodeMemberField AddField(CodeTypeReference type, string fieldName, MemberAttributes attributes = MemberAttributes.Public)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1} {2}", type.BaseType, fieldName, "field", Tags.CreationField, Tags.CodeDom);

            if (type.IsNull())
                throw this.ArgumentNull("type");

            if (fieldName.IsNullOrEmpty())
                throw this.ArgumentNull("fieldName");

            CodeMemberField member = new CodeMemberField();
            CurrentMember = member;
            CurrentType.Members.Add(member);
            //IBoxStack<CodeTypeMember> result = Members.AddBlock(member);
            member.Name = fieldName;
            member.Type = type;
            member.Attributes = attributes;
            return member;
        }


        public IBoxStack<CodeMemberMethod> AddMethod(string typeName, string methodName, MemberAttributes attributes = MemberAttributes.Public)
        {

            if (typeName.IsNullOrEmpty())
                throw this.ArgumentNull("typeName");

            if (methodName.IsNullOrEmpty())
                throw this.ArgumentNull("methodName");

            var type = Devtm.Writers.TypeReference.Get(typeName);
            var m = AddMethod(type, methodName, attributes);
            return m;
        }


        public IBoxStack<CodeMemberMethod> AddMethod(Type type, string methodName, MemberAttributes memberAttributes)
        {

            if (methodName.IsNullOrEmpty())
                throw this.ArgumentNull("methodName");

            return AddMethod(type.TypeReference(), methodName, memberAttributes);
        }


        public IBoxStack<CodeMemberMethod> AddMethod(CodeTypeReference type, string methodName, MemberAttributes attributes = MemberAttributes.Public)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1} {2}", type.BaseType, methodName, "method", Tags.CreationMethod, Tags.CodeDom);

            if (methodName.IsNullOrEmpty())
                throw this.ArgumentNull("methodName");

            if (type.IsNull())
                throw this.ArgumentNull("type");

            CodeMemberMethod member = new CodeMemberMethod();
            CurrentMember = member;
            CurrentType.Members.Add(member);
            IBoxStack<CodeTypeMember> result = Members.AddBlock(member);
            member.Name = methodName;
            member.ReturnType = type;
            member.Attributes = attributes;
            CurrentStatement = member.Statements;
            return result.ConvertIn<CodeMemberMethod>();
        }


        public IBoxStack<CodeConstructor> AddCtor(MemberAttributes attributes = MemberAttributes.Public)
        { 
            return AddCtor(null, attributes);
        }
        public IBoxStack<CodeConstructor> AddCtor(IEnumerable<CodeParameterDeclarationExpression> parameters, MemberAttributes attributes = MemberAttributes.Public)
        {

            CodeConstructor member = new CodeConstructor();

            if (parameters != null && parameters.Count() > 0)
                member.Parameters.AddRange(parameters.ToArray());

            CurrentMember = member;
            CurrentType.Members.Add(member);
            IBoxStack<CodeTypeMember> result = Members.AddBlock(member);
            member.Attributes = attributes;
            CurrentStatement = member.Statements;

            return result.ConvertIn<CodeConstructor>();

        }


        public IBoxStack<CodeMemberEvent> AddEvent(string typeName, string eventName, MemberAttributes attributes = MemberAttributes.Public)
        {

            if (typeName.IsNullOrEmpty())
                throw this.ArgumentNull("typeName");

            if (eventName.IsNullOrEmpty())
                throw this.ArgumentNull("eventName");

            var type = Devtm.Writers.TypeReference.Get(typeName);
            return AddEvent(type, eventName, attributes);
        }


        public IBoxStack<CodeMemberEvent> AddEvent(CodeTypeReference type, string eventName, MemberAttributes attributes = MemberAttributes.Public)
        {

            this.Log(LogLevelEnm.Trace, "Create {0} {1} {2}", type.BaseType, eventName, "Event", Tags.CreationEvent, Tags.CodeDom);

            if (type.IsNull())
                throw this.ArgumentNull("className");

            if (eventName.IsNullOrEmpty())
                throw this.ArgumentNull("eventName");
            
            CodeMemberEvent member = new CodeMemberEvent();
            CurrentMember = member;
            CurrentType.Members.Add(member);
            IBoxStack<CodeTypeMember> result = Members.AddBlock(member);
            member.Name = eventName;
            member.Type = type;
            member.Attributes = attributes;
            return result.ConvertIn<CodeMemberEvent>();
        }


        public CodeExpression This()
        {
            return new CodeThisReferenceExpression();
        }


        public CodeExpression Base()
        {
            return new CodeBaseReferenceExpression();
        }


        public string extension { get; private set; }


        public void Enter(object container)
        {

            CodeTypeDeclaration t = container as System.CodeDom.CodeTypeDeclaration;
            if (t != null)
            {
                string fullNamespace = GetFullnameCurrentType();
                project.Types.Add(fullNamespace);

            }

        }


        private string GetFullnameCurrentType()
        {

            string fullNamespace = string.Empty;
            foreach (string item in Namespaces.Select(c => c.Item.Name))
            {
                if (!string.IsNullOrEmpty(fullNamespace))
                    fullNamespace += ".";
                fullNamespace += item;
            }

            string type = string.Empty;
            foreach (string item in Types.Select(c => c.Item.Name))
            {
                if (!string.IsNullOrEmpty(fullNamespace))
                    type += ".";
                type += item;
            }

            if (!string.IsNullOrEmpty(fullNamespace))
                fullNamespace += ".";

            fullNamespace += type;
            return fullNamespace;
        }


        public void Exit(object container)
        {

            if (container.IsNull())
                throw this.ArgumentNull("container");


            if (container is CodeNamespace)
                LastNamespace = container as CodeNamespace;
            else if (container is CodeTypeDeclaration)
                LastType = container as CodeTypeDeclaration;
            if (container is CodeTypeMember)
                LastMember = container as CodeTypeMember;


            if (container == CurrentStatement)
            {

                var m = CurrentMember as CodeMemberMethod;

                if (m != null)
                {

                    CurrentStatement = m.Statements;
                }

            }
        }


        public void RemoveNamespace(CodeNamespace codeNamespace)
        {

            this.Log(LogLevelEnm.Trace, "Remove {0} {1}", codeNamespace.Name, "namespace", Tags.RemoveNamespace);

            if (codeNamespace.IsNull())
                throw this.ArgumentNull("codeNamespace");


            foreach (CodeTypeDeclaration item in codeNamespace.Types)
                RemoveType(item);

            CompileUnit.Namespaces.Remove(codeNamespace);

        }


        public void RemoveNamespace(StackContainer<CodeNamespace> n)
        {

            this.Log(LogLevelEnm.Trace, "Remove {0} {1}", n.Item.Name, "namespace", Tags.RemoveNamespace, Tags.CodeDom);

            if (n.IsNull())
                throw this.ArgumentNull("n");

            foreach (CodeTypeDeclaration item in n.Item.Types)
                RemoveType(item);

            CompileUnit.Namespaces.Remove(n.Item);

        }


        public void RemoveType(CodeTypeDeclaration t)
        {

            this.Log(LogLevelEnm.Trace, "Remove {0} {1}", t, "Type", Tags.RemoveType);

            if (t.IsNull())
                throw this.ArgumentNull("t");

            if (CurrentNamespace.Types.Contains(t))
                CurrentNamespace.Types.Remove(t);

            else if (CurrentType.Members.Contains(t))
                CurrentType.Members.Remove(t);

            string fullNamespace = GetFullnameCurrentType();

            project.Types.Remove(fullNamespace);

        }


        public void RemoveType(IBoxStack<CodeTypeDeclaration> t)
        {

            this.Log(LogLevelEnm.Trace, "Remove {0} {1}", t, "Type", Tags.RemoveType, Tags.CodeDom);

            if (t.IsNull())
                throw this.ArgumentNull("t");

            if (CurrentNamespace.Types.Contains(t.Item))
                CurrentNamespace.Types.Remove(t.Item);

            else if (CurrentType.Members.Contains(t.Item))
                CurrentType.Members.Remove(t.Item);

            string fullNamespace = GetFullnameCurrentType();
            project.Types.Remove(fullNamespace);

        }


        public EnumType ContentType
        {
            get { return EnumType.Code; }
        }

    }

}
